import chalk from 'chalk'
import ora from 'ora'
import shell from 'shelljs'
import { NodeSSH } from 'node-ssh'
import inquirer, { QuestionCollection } from 'inquirer'
import zipFile from 'compressing'
import path from 'path'
import dayjs from 'dayjs'
import { exec } from 'child_process'
import spinnerStyle from './utils/spinner_style'
import { to, toType, defaultLog, errorLog, successLog, timeLog, timeEndLog, geoBuiltIn, notice, isValidKey } from './utils/index'
import { choicesDTO, deployListDTO, Params } from './utils/validatorParam'
import { builtIn, models } from './config'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'
import { deploy } from './typings/DeployProps'

class Deploy implements deploy.DeployProps {
  options: Params
  SSH: NodeSSH
  deployConfig!: deployListDTO
  name!: string
  runPath: string
  model!: deployListDTO['RUN_MODEL']
  distDir?: string
  distZipPath?: string

  constructor(options: Params) {
    this.options = options
    this.SSH = new NodeSSH()

    // 工作目录
    this.runPath = process.cwd()

    // 参数校验
    this._validate()
      .then(() => {
        try {
          // 开始运行
          this._run()
        } catch (error) {
          this._quit(1)
        }
      })
      .catch(() => {
        this._quit(1)
      })
  }

  /**
   * 运行入口
   * @static
   * @param {Params} options
   * @returns {Deploy}
   * @memberof Deploy
   */
  static instantiate(options: Params): Deploy {
    return new Deploy(options)
  }

  /**
   * 运行
   * @memberof Deploy
   */
  async _run(): Promise<void> {
    const selectList = {
      type: 'list',
      message: '请选择发布环境',
      name: 'env',
      choices: this.options.choices
    } as QuestionCollection

    // 执行交互后 启动发布程序
    inquirer.prompt(selectList).then(async(answers) => {
      const match = this.options.choices.find((e) => e.value === answers.env) as choicesDTO

      this.deployConfig = this.options.deployList.find(e => e.VALUE === answers.env) as deployListDTO
      this.model = this.deployConfig.RUN_MODEL
      this.name = `${this.options.name}-${match.name}`

      if (this.deployConfig.ASSETS_PATH) {
        this.distDir = path.resolve(this.runPath, this.deployConfig.ASSETS_PATH) // 待打包
        this.distZipPath = path.resolve(this.runPath, 'dist.zip') // 打包后地址(dist.zip是文件名,不需要更改, 主要在config中配置 PATH 即可)
      }

      // 部署
      const [err, res] = await to(this._autoDeploy())
      const model = this.model as number
      if (!err && res && isValidKey(model, models)) {
        const modelObj = models[model] as any
        notice(
          `${this.name}${modelObj.msg}`,
          `${this.name}${modelObj.msg}完成}`,
          this
        )
      }

      // 断开连接
      this.SSH.dispose()

      this._quit(0)
    })
  }

  /**
   * 部署
   * @returns
   * @memberof Deploy
   */
  async _autoDeploy(): Promise<boolean> {
    const { model } = this
    let runProcess = []

    if (model !== 4) {
      for (let i = 0; i < model; i++) {
        const { run, label } = builtIn[i + 1]
        runProcess.push(geoBuiltIn<Deploy>(this, run, label))
      }
    } else {
      if (!toType(this.deployConfig.SERVER_CMD, 'Array')) {
        this.deployConfig.SERVER_CMD = []
      }

      runProcess = (this.deployConfig.SERVER_CMD as []).map(e => {
        return geoBuiltIn(this, e)
      })
    }

    console.log(chalk.yellow('--------->  前端自动部署工具  <---------'))
    timeLog('总时间：')
    for (const e of runProcess) {
      const label = (e as any).label
      const fn = (e as any).fn
      timeLog(label)
      await fn.call(this)
      timeEndLog(label)
      console.log('\n')
    }

    successLog('--------->  部署成功!  <---------')
    timeEndLog('总时间：')
    successLog(dayjs().format('YYYY-MM-DD HH:mm:ss'))

    return true
  }

  /**
   * 参数校验
   * @returns {Promise<void>}
   * @memberof Deploy
   */
  async _validate(): Promise<boolean> {
    const param = plainToClass(Params, this.options)
    let loading: ora.Ora
    const [err, res] = await to(validate(param), {
      before: () => {
        loading = ora(defaultLog('正在校验参数中')).start()
        loading.spinner = spinnerStyle.betaWave
      },
      after: () => {
        loading.stop()
      }
    })
    if (!res.length) {
      successLog('参数校验成功')
      return true
    } else {
      notice('参数校验', '参数校验错误，请查看控制台输出日志内容进行修正！', this)
      const [start] = res
      const errMsg = `参数校验错误，${start.property}字段，${start.toString()}}`
      errorLog(errMsg)
      return Promise.reject(new Error(errMsg))
    }
  }

  /**
   * 打包阶段
   * @returns
   * @memberof Deploy
   */
  async _compileDist(): Promise<void> {
    shell.cd(path.resolve(this.runPath))

    const loading = ora(defaultLog('项目开始打包')).start()
    loading.spinner = spinnerStyle.betaWave
    // 执行shell 打包命令
    const res = shell.exec(this.deployConfig.SCRIPT as string)
    loading.stop()

    if (res.code === 0) {
      successLog('项目打包成功!')
    } else {
      notice(`${this.name}的项目打包错误`, '项目打包失败, 请重试!', this)
      errorLog('项目打包失败, 请重试!')
      return Promise.reject(new Error('项目打包失败, 请重试!'))
    }
  }

  /**
   * 代码压缩阶段
   * @returns
   * @memberof Deploy
   */
  async _zipDist(): Promise<void> {
    let loading: ora.Ora
    // 文件夹目录
    const [err] = await to(
      zipFile.zip.compressDir(this.distDir as string, this.distZipPath as string), {
        before: () => {
          loading = ora(defaultLog('项目开始压缩')).start()
          loading.spinner = spinnerStyle.betaWave
        },
        after: () => {
          loading.stop()
        }
      }
    )
    if (!err) {
      successLog('压缩成功!')
    } else {
      errorLog(err)
      notice(`${this.name}的压缩代码错误`, '压缩失败, 退出程序!', this)
      errorLog('压缩失败, 退出程序!')
      return Promise.reject(new Error('压缩失败, 退出程序!'))
    }
  }

  /**
   * 连接服务器阶段
   * @returns
   * @memberof Deploy
   */
  async _connectSSH(): Promise<boolean | void> {
    if (this.SSH.isConnected()) {
      return successLog('服务器已经连接，无序再次连接')
    }
    let loading: ora.Ora
    const [err] = await to(
      this.SSH.connect({
        host: this.deployConfig.SERVER_PATH,
        username: this.deployConfig.SSH_USER,
        password: this.deployConfig.PASSWORD // 密码登录 方式二
      }),
      {
        before: () => {
          loading = ora(defaultLog('正在连接服务器')).start()
          loading.spinner = spinnerStyle.betaWave
        },
        after: () => {
          loading.stop()
        }
      }
    )
    if (!err) {
      successLog('服务器连接成功!')
    } else {
      errorLog(err)
      notice(`${this.name}的连接服务器错误`, 'SSH连接失败!', this)
      errorLog('SSH连接失败!')
      return Promise.reject(new Error('SSH连接失败!'))
    }
  }

  /**
   * 传送zip文件到服务器
   * @returns
   * @memberof Deploy
   */
  async _uploadZipBySSH(): Promise<void> {
    // 连接ssh
    await this._connectSSH()
    // 线上目标文件清空
    await this._clearOldFile()

    let loading: ora.Ora
    const [err] = await to(
      this.SSH.putFiles([
        // local 本地 ; remote 服务器 ;
        {
          local: this.distZipPath as string,
          remote: this.deployConfig.PATH + '/dist.zip'
        }
      ]),
      {
        before() {
          loading = ora(defaultLog('准备上传文件')).start()
          loading.spinner = spinnerStyle.betaWave
        },
        after() {
          loading.stop()
        }
      }
    )
    if (!err) {
      successLog('上传成功!')
      defaultLog('正在解压文件')
      await this._runCommand('unzip ./dist.zip') // 解压
      await this._runCommand(`rm -rf ${this.deployConfig.PATH}/dist.zip`) // 解压完删除线上压缩包
      await this._runCommand(
        `mv -f ${this.deployConfig.PATH}/${this.deployConfig.ASSETS_PATH}/*  ${this.deployConfig.PATH}`
      )
      await this._runCommand(`rm -rf ${this.deployConfig.PATH}/${this.deployConfig.ASSETS_PATH}`) // 移出后删除 dist 文件夹
    } else {
      errorLog(err)
      notice(`${this.name}的连接SSH错误`, '上传失败!', this)
      errorLog('上传失败!')
      return Promise.reject(new Error('上传失败!'))
    }
  }

  /**
   * 线上执行命令
   * @param {String} command 命令操作 如 ls
   * @memberof Deploy
   */
  async _runCommand(command: string): Promise<void> {
    if (!toType(command, 'String')) {
      notice(`${this.name}的自定义指令`, `"${command}"指令操作失败!`, this)
      errorLog('指令必须是字符串')
      return Promise.reject(new Error('指令必须是字符串'))
    }
    let loading: ora.Ora
    const [error, result] = await to(
      this.SSH.exec(command, [], {
        cwd: this.deployConfig.PATH,
        stream: 'both'
      }),
      {
        before: () => {
          loading = ora(defaultLog(`正在执行${command}指令`)).start()
          loading.spinner = spinnerStyle.betaWave
        },
        after: () => {
          loading.stop()
        }
      }
    )

    if (!error) {
      successLog(`\n ${result.stdout}，\n ${command}指令成功! \n`)
    } else {
      errorLog(error)
      notice(`${this.name}的自定义指令`, `"${command}"指令操作失败!`, this)
      errorLog(`自定义指令，"${command}"指令操作失败!`)
      return Promise.reject(new Error(`自定义指令，"${command}"指令操作失败!`))
    }
  }

  /**
   * 清空线上目标目录里的旧文件
   * @memberof Deploy
   */
  async _clearOldFile(): Promise<void> {
    try {
      const commands = ['ls', 'rm -rf *']
      await Promise.all(commands.map((it) => this._runCommand(it)))
    } catch (error) {
      errorLog(error)
      notice(`${this.name}的操作线上目录错误`, '操作失败!', this)
      errorLog('清空线上目标目录里的旧文件，操作失败!')
      return Promise.reject(new Error('清空线上目标目录里的旧文件，操作失败!'))
    }
  }

  /**
   * 退出程序
   * @param {(0 | 1)} num - 0正常退出，1异常退出
   * @memberof Deploy
   */
  _quit(num: 0 | 1): void {
    process.exitCode = num // 正常退出
  }
}

export default Deploy.instantiate
